[Solana] NFTをmintしてみる
Introduction
以前の記事では、Solanaのスマートコントラクトを試してみました。
今回は、同じくAnchorでスマートコントラクトを記述して
Metaplexを使ってNFTを作成してみます。
環境(Rust、Anchor、Solana wallet)については前回の記事を参照してください。
Environment
- rust : 1.62.1
- Node : 18.7
- yarn : 1.22.17
- Anchor : 0.24.2
Try
ここの記事を参考に、NFTをmintしてみます。
まずはsolana configコマンドで、対象をdevnetに設定しておきます。
% solana config set --url devnet Config File: ・・・ RPC URL: https://api.devnet.solana.com WebSocket URL: wss://api.devnet.solana.com/ (computed) Keypair Path: /path/your/.config/solana/id.json Commitment: confirmed
Keypairファイルのパスを確認しておきましょう。
まだKeypairがない場合、solana-keygen newコマンドで新たに生成しておきます。
solana-keygen pubkeyコマンドでpublic keyが表示されます。
%solana-keygen pubkey xxxxxxxxxxxxxxxxxxxxxxxxxxx
NFTをmintするのに必要になるので、SOLを少しdropしておきます。
2〜3SOLくらいあればたぶんOK。
#何度かairdrop % solana airdrop 1 Requesting airdrop of 1 SOL % solana balance 1 SOL
Anchorプロジェクト作成
アンカーCLIを使用して、次のコマンドでアンカープロジェクトを作成します。
% anchor init anchror-example yarn install v1.22.15 warning package.json: No license field info No lockfile found. ・・・・
Anchor.tomlのcluster設定もdevnetにします。
## anchror-example/Anchor.toml ・・・ [provider] cluster = "devnet" wallet = "<さっき作成したkeypairのフルパス>" ・・・
rustのCargo.tomlで依存ライブラリを設定します。
# programs/<your-project-name>/Cargo.toml [dependencies] anchor-lang = "0.24.2" anchor-spl = "0.24.2" mpl-token-metadata = {version = "1.2.7", features = ["no-entrypoint"]}
これでmint関数を作成できます。
src/lib.rsを次のように記述します。
※ここのソースほぼそのまま
use anchor_lang::prelude::*; use anchor_lang::solana_program::program::invoke; use anchor_spl::token; use anchor_spl::token::{MintTo, Token}; use mpl_token_metadata::instruction::{create_master_edition_v3, create_metadata_accounts_v2}; #[derive(Accounts)] pub struct MintNFT<'info> { #[account(mut)] pub mint_authority: Signer<'info>, /// CHECK: This is not dangerous because we don't read or write from this account #[account(mut)] pub mint: UncheckedAccount<'info>, // #[account(mut)] pub token_program: Program<'info, Token>, /// CHECK: This is not dangerous because we don't read or write from this account #[account(mut)] pub metadata: UncheckedAccount<'info>, /// CHECK: This is not dangerous because we don't read or write from this account #[account(mut)] pub token_account: UncheckedAccount<'info>, /// CHECK: This is not dangerous because we don't read or write from this account pub token_metadata_program: UncheckedAccount<'info>, /// CHECK: This is not dangerous because we don't read or write from this account #[account(mut)] pub payer: AccountInfo<'info>, pub system_program: Program<'info, System>, /// CHECK: This is not dangerous because we don't read or write from this account pub rent: AccountInfo<'info>, /// CHECK: This is not dangerous because we don't read or write from this account #[account(mut)] pub master_edition: UncheckedAccount<'info>, } pub fn mint_nft( ctx: Context<MintNFT>, creator_key: Pubkey, uri: String, title: String, ) -> Result<()> { msg!("Initializing Mint NFT"); let cpi_accounts = MintTo { mint: ctx.accounts.mint.to_account_info(), to: ctx.accounts.token_account.to_account_info(), authority: ctx.accounts.payer.to_account_info(), }; msg!("CPI Accounts Assigned"); let cpi_program = ctx.accounts.token_program.to_account_info(); msg!("CPI Program Assigned"); let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); msg!("CPI Context Assigned"); token::mint_to(cpi_ctx, 1)?; msg!("Token Minted"); let account_info = vec![ ctx.accounts.metadata.to_account_info(), ctx.accounts.mint.to_account_info(), ctx.accounts.mint_authority.to_account_info(), ctx.accounts.payer.to_account_info(), ctx.accounts.token_metadata_program.to_account_info(), ctx.accounts.token_program.to_account_info(), ctx.accounts.system_program.to_account_info(), ctx.accounts.rent.to_account_info(), ]; msg!("Account Info Assigned"); let creator = vec![ mpl_token_metadata::state::Creator { address: creator_key, verified: false, share: 100, }, mpl_token_metadata::state::Creator { address: ctx.accounts.mint_authority.key(), verified: false, share: 0, }, ]; msg!("Creator Assigned"); let symbol = std::string::ToString::to_string("symb"); invoke( &create_metadata_accounts_v2( ctx.accounts.token_metadata_program.key(), ctx.accounts.metadata.key(), ctx.accounts.mint.key(), ctx.accounts.mint_authority.key(), ctx.accounts.payer.key(), ctx.accounts.payer.key(), title, symbol, uri, Some(creator), 1, true, false, None, None, ), account_info.as_slice(), )?; msg!("Metadata Account Created !!!"); let master_edition_infos = vec![ ctx.accounts.master_edition.to_account_info(), ctx.accounts.mint.to_account_info(), ctx.accounts.mint_authority.to_account_info(), ctx.accounts.payer.to_account_info(), ctx.accounts.metadata.to_account_info(), ctx.accounts.token_metadata_program.to_account_info(), ctx.accounts.token_program.to_account_info(), ctx.accounts.system_program.to_account_info(), ctx.accounts.rent.to_account_info(), ]; msg!("Master Edition Account Infos Assigned"); invoke( &create_master_edition_v3( ctx.accounts.token_metadata_program.key(), ctx.accounts.master_edition.key(), ctx.accounts.mint.key(), ctx.accounts.payer.key(), ctx.accounts.mint_authority.key(), ctx.accounts.metadata.key(), ctx.accounts.payer.key(), Some(0), ), master_edition_infos.as_slice(), )?; msg!("Master Edition Nft Minted !!!"); Ok(()) } declare_id!("<Program ID>"); #[program] pub mod anchror_example { use super::*; pub fn initialize(ctx: Context<Initialize>) -> Result<()> { Ok(()) } } #[derive(Accounts)] pub struct Initialize {}
NFT用の構造体とmintする関数を定義しています。
プログラムをデバッグする場合はmsg!()を使い、
ログはターミナル画面か.anchor/program-logs/
ファイルを記述したらビルド&デプロイ。
% anchor build && anchor deploy Deploying workspace: https://api.devnet.solana.com Upgrade authority: /path/your/my-solana-wallet/my-keypair.json Deploying program "anchror-example"... Program path: /path/your/anchror-example/target/deploy/anchror_example.so... Program Id: xxxxxxxxxxxxxxxxxxxxxx Deploy success
デプロイが成功すると↑のようにProgram Idが表示されます。
このIDをAnchor.tomlとlib.rsのdeclare_idに記述します。
なお、プログラムを修正して再度ビルド/デプロイしたい場合、
anchor cleanしてtarget以下を削除してから再度ビルドしましょう。
次にmochaテストを実行することでデプロイしたスマートコントラクトを実行し、
NFTをmintします。
yarnかnpmで必要なライブラリをインストールします。
% yarn add @solana/web3.js % yarn add @solana/spl-token % yarn add ts-mocha
テストファイル(tests/test.ts)を作成し、
↓のように作成。こちらもこれほぼそのままです。
import * as anchor from '@project-serum/anchor' import { Program, Wallet } from '@project-serum/anchor' import { Example } from '../target/types/example' import { TOKEN_PROGRAM_ID, createAssociatedTokenAccountInstruction, getAssociatedTokenAddress, createInitializeMintInstruction, MINT_SIZE } from '@solana/spl-token' // IGNORE THESE ERRORS IF ANY const { SystemProgram } = anchor.web3 describe('metaplex-anchor-nft', () => { // Configure the client to use the local cluster. const provider = anchor.AnchorProvider.env(); const wallet = provider.wallet as Wallet; anchor.setProvider(provider); const program = anchor.workspace.Example as Program<Example> it("Is initialized!", async () => { const TOKEN_METADATA_PROGRAM_ID = new anchor.web3.PublicKey( "<Public Key>" ); const lamports: number = await program.provider.connection.getMinimumBalanceForRentExemption( MINT_SIZE ); const getMetadata = async ( mint: anchor.web3.PublicKey ): Promise<anchor.web3.PublicKey> => { return ( await anchor.web3.PublicKey.findProgramAddress( [ Buffer.from("metadata"), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer(), ], TOKEN_METADATA_PROGRAM_ID ) )[0]; }; const getMasterEdition = async ( mint: anchor.web3.PublicKey ): Promise<anchor.web3.PublicKey> => { return ( await anchor.web3.PublicKey.findProgramAddress( [ Buffer.from("metadata"), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer(), Buffer.from("edition"), ], TOKEN_METADATA_PROGRAM_ID ) )[0]; }; const mintKey: anchor.web3.Keypair = anchor.web3.Keypair.generate(); const NftTokenAccount = await getAssociatedTokenAddress( mintKey.publicKey, wallet.publicKey ); console.log("NFT Account: ", NftTokenAccount.toBase58()); const mint_tx = new anchor.web3.Transaction().add( anchor.web3.SystemProgram.createAccount({ fromPubkey: wallet.publicKey, newAccountPubkey: mintKey.publicKey, space: MINT_SIZE, programId: TOKEN_PROGRAM_ID, lamports, }), createInitializeMintInstruction( mintKey.publicKey, 0, wallet.publicKey, wallet.publicKey ), createAssociatedTokenAccountInstruction( wallet.publicKey, NftTokenAccount, wallet.publicKey, mintKey.publicKey ) ); const res = await program.provider.sendAndConfirm(mint_tx, [mintKey]); console.log( await program.provider.connection.getParsedAccountInfo(mintKey.publicKey) ); console.log("Account: ", res); console.log("Mint key: ", mintKey.publicKey.toString()); console.log("User: ", wallet.publicKey.toString()); const metadataAddress = await getMetadata(mintKey.publicKey); const masterEdition = await getMasterEdition(mintKey.publicKey); console.log("Metadata address: ", metadataAddress.toBase58()); console.log("MasterEdition: ", masterEdition.toBase58()); const tx = await program.methods.mintNft( mintKey.publicKey, <Metadataのパス>, "My Icon", ) .accounts({ mintAuthority: wallet.publicKey, mint: mintKey.publicKey, tokenAccount: NftTokenAccount, tokenProgram: TOKEN_PROGRAM_ID, metadata: metadataAddress, tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID, payer: wallet.publicKey, systemProgram: SystemProgram.programId, rent: anchor.web3.SYSVAR_RENT_PUBKEY, masterEdition: masterEdition, }, ) .rpc(); console.log("Your transaction signature", tx); }); });
mintNftの第2引数にはmetadataのパスを指定します。
このmetadataには画像のパスや名前、属性などのメタ情報を記述します。
metadata・NFT画像はarweaveやShadow Driveに置いておくことが多いみたいです。
ここでやっているように、metadataをarweaveにアップしてもよいですが、
今回はとりあえずサンプルにのっている
https://arweave.net/y5e5DJsiwH0s_ayfMwYk-SnrZtVZzHLQDSTZ5dNRUHA
を指定しておきます。
そしてanchor test実行でNFTをmintします。
% anchor test BPF SDK: /path/your/.local/share/solana/install/releases/1.10.25/solana-release/bin/sdk/bpf cargo-build-bpf child: rustup toolchain list -v cargo-build-bpf child: cargo +bpf build --target bpfel-unknown-unknown --release Finished release [optimized] target(s) in 0.38s To deploy this program: Deploying workspace: https://api.devnet.solana.com Deploying program "example"... Program Id: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Deploy success NFT Account: xxxxxxxxxxxxxxxx { context: { apiVersion: '1.10.25', slot: 141731498 }, value: { data: { parsed: [Object], program: 'spl-token', space: 82 }, executable: false, lamports: 1461600, owner: PublicKey { _bn: <BN: xxxxxxxxxxxxxxxxxx> }, rentEpoch: 328 } } Account: xxxxxxxxxxxxxx Mint key: xxxxxxxxxxx User: xxxxxxxxxxxx Metadata address: xxxxxxxxxxxx MasterEdition: xxxxxxxxxxxxxx Your transaction signature xxxxxxxxxxxx ✔ Is initialized! (5484ms) 1 passing (5s) ✨ Done in 9.85s.
コードに問題なければ、mintされます。
Solscanを使えば、登録したNFTを確認することもできます。
↑のmint keyを使い、下記のURLをブラウザで表示すると登録したNFTが表示されます。
https://solscan.io/token/<Mint Key>?cluster=devnet#txs
Summary
このような感じで、けっこう簡単にNFTを登録ができました。
今回の宛先はDevnet(開発用)でしたが、
本番用に向けて実際のSOLを使えば本番環境にNFTを登録し、
値段をつけて販売することも可能になります。